"""
Copyright 2007 VMware, Inc.  All rights reserved. -- VMware Confidential

@todo: Can probably make it faster by moving to the right location
instead of clearing the entire line every time.  Increases in rxvt are
very choppy when it progresses very fast.  Might really show over a
slow ssh connection.
"""
import curses
import math
import signal
import sys
import time
import os

from subprocess import Popen, PIPE
from textwrap import TextWrapper

from vmis.db import db
from vmis.core.common import State
from vmis.core.errors import ValidationError, ValidationErrorNonFatal, EULADeclinedError
from vmis.core.questions import ValidationError, QUESTION_DIRECTORY, \
                                QUESTION_YESNO, QUESTION_NUMERIC, \
                                QUESTION_CLOSEPROGRAMS, QUESTION_TEXTENTRY, \
                                QUESTION_SHUTDOWNPROGRAM, QUESTION_PORTENTRY, \
                                QUESTION_DUALPORTENTRIES
from vmis.ui import MessageTypes
from vmis.ui.uiAppControl import UIAppControl
from vmis.util import Format
from vmis.util.log import getLog
from vmis.util.path import path
from vmis.util.shell import Which, run

log = getLog('vmis.ui.console')

PRIMARY_LINE = 0
SECONDARY_LINE = 1
PROGRESS_LINE = 2

class EULA(State):
   @staticmethod
   def Show(txn, eula):
      """
      Show the EULA to the user and get acceptance of it.
      """
      answer = eula.GetDefault()

      while True:
         answer = txn.ui.ShowEULA(eula.text, eula.componentName)
         try:
            eula.Validate(answer)
            txn.Next()
            break
         except EULADeclinedError:
            raise     # When the user explicitly declines we bail out.
         except ValidationError, e:
            print >> sys.stderr, Format(e.message)

def TextEntry(question, text, default, ui):
   """
   Populate a text entry

   @param question: The question object attached to this call
   @param text: Text to display. Not used in this function.
   @param default: Default entry.  Not used in this function
   @param ui: The user interface object.  Not used in this function

   @return: (Text with which to prompt the user, None)
   """
   text = "%s\n%s\n" % (question.header, question.footer)
   return (text, None)


def ShutdownProgram(question, text, default, ui):
   """
   Detect and shutdown a specific program

   @param question: The question object attached to this call
   @param text: Text to display. Not used in this function.
   @param default: Default entry.  Not used in this function
   @param ui: The user interface object

   @return: (None, None) - Move on to the next question when done
   """
   programShutdown = False
   while not programShutdown:
      ret = run('/bin/sh', '-c', '/bin/ps -e | grep %s' % question.program, ignoreErrors=True)['stdout']

      if ret:
         # Found some processes that match the criteria.  Prompt the user and stay
         # in the loop
         text = 'The VMware Installer cannot continue while %s ' % question.programName + \
          'is running.  Please shut it down and press <Enter> to continue.'
         answer = ui.Prompt(text, None, format=False)
      else:
         # Otherwise we're good.
         programShutdown = True
   return (None, None)


def ClosePrograms(question, text, default, ui):
   """
   Display the close programs and VMs question.

   @param question: The question object attached to this call
   @param text: Text to display
   @param default: Default entry
   @param ui: The user interface object

   @return: (None, None) - Move on to the next question when done
   """
   # Loop until there are no open items
   appControlSucceeded = False
   while True:
      try:
         text = ""

         ui.appControl.Initialize()

         openList = []

         # If there are open items, retrieve their names.
         if (ui.appControl.numVMs > 0):
            for i in range(ui.appControl.numVMs):
               name = ui.appControl.GetVMInfo(i)
               openList.append(name)

         if (ui.appControl.numApps > 0):
            for i in range(ui.appControl.numApps):
               (name, product) = ui.appControl.GetAppInfo(i)
               openList.append(name)

         if openList:
            text = 'The following virtual machines and VMware applications\n' + \
                   'are running.  Please suspend or close them or press\n' + \
                   '\'enter\' to do this automatically\n\n'
            for item in openList:
               text = text + '> %s\n' % item

            answer = ui.Prompt(text, None, format=False)
            ui.appControl.ShutdownAll()
            appControlSucceeded = True
         else:
            return (None, None)
      except Exception, e:
         log.info('Cannot use vmware-app-control to shut down open VMs, defaulting to fallback message.')
         log.debug('Exception: %s' % e)

      # Fall back on our old method.  It can't detect running UIs, only running VMs, but it's
      # better than nothing.  Only allow installation to continue when no more VMs are running.
      if question.checkVMsRunning():
         if appControlSucceeded:
            text = 'The VMware Installer could not shut down all running virtual ' + \
                   'machines.  If you have encrypted VMs open, please shut them down or ' + \
                   'suspend them now and press \'enter\' to continue.'
         else:
            text = 'The VMware Installer cannot continue if there are running virtual\n' + \
                   'machines. Shut down or suspend running virtual machines before\n' + \
                   'continuing.  Press \'enter\' to continue.'
         answer = ui.Prompt(text, None, format=False)
      else:
         return (None, None)

def PortEntry(question, text, default, ui):
   """
   Grab a port entry

   @param question: The question object attached to this call
   @param text: Text to display. Not used in this function.
   @param default: Default entry. Not used in this function
   @param ui: The user interface object

   @return: (None, Answer)
   """
   while 1:
      port = ''
      if default:
         port = default

      answer = ui.Prompt('%s (%s)' % (text, question.label), port, format=True)
      try:
         question.Validate(answer)
         return (None, answer)
      except ValidationErrorNonFatal, e:
         # An explicitly non-fatal error.
         return (None, answer)
      except ValidationError, e:
         print e

def DualPortEntries(question, text, default, ui):
   """
   Grab two port entries

   @param question: The question object attached to this call
   @param text: Text to display. Not used in this function.
   @param default: Default entry.  Not used in this function
   @param ui: The user interface object

   @return: (None, Answer)
   """
   portsValid = False
   while not portsValid:
      port1 = ''
      port2 = ''
      if default: # Parse it out
         ports = default.split('/')
         if len(ports) == 2:
            port1 = ports[0]
            port2 = ports[1]

      string = '%s (%s)' % (text, question.label1)
      answer1 = ui.Prompt(string, port1, format=True)
      string = '%s (%s)' % (text, question.label2)
      answer2 = ui.Prompt(string, port2, format=True)
      # This will throw an exception if the answers are invalid.
      try:
         answer = '%s/%s' % (answer1, answer2)
         question.Validate(answer)
         return (None, answer)
      except ValidationErrorNonFatal, e:
         # An explicitly non-fatal error.  Return our answer.
         return (None, answer)
      except ValidationError, e:
         print e


# Mapping between question types and their functions.
questionFunctions = { QUESTION_DIRECTORY : None,
                      QUESTION_YESNO : None,
                      QUESTION_NUMERIC : None,
                      QUESTION_CLOSEPROGRAMS: ClosePrograms,
                      QUESTION_SHUTDOWNPROGRAM: ShutdownProgram,
                      QUESTION_TEXTENTRY: TextEntry,
                      QUESTION_PORTENTRY: PortEntry,
                      QUESTION_DUALPORTENTRIES: DualPortEntries,}

class Question(State):
   @staticmethod
   def Show(txn, question):
      while True:
         # Allow for more elaborate prompts
         func = questionFunctions.get(question.type, None)
         answer = None
         if func != None:
            (text, answer) = func(question, question.text, question.GetDefault(), txn.ui)
         else:
            text = question.text
         # If an answer hasn't been set, check the text entry
         if not answer:
            if text is None:
               # If the returned text was None, move on to the next question.
               txn.Next()
               break
            else:
               answer = txn.ui.Prompt(text, question.GetDefault())
         try:
            answer = question.Validate(answer)
            db.config.Set(question.component, question.key, answer)
            txn.Next()
            break
         except ValidationErrorNonFatal, e:
            print >> sys.stderr, Format(e.message)
            # This is an explicitly non-fatal case.  Continue with the given answers.
            db.config.Set(question.component, question.key, answer)
            txn.Next()
            break
         except ValidationError, e:
            print >> sys.stderr, Format(e.message)

class Finish(State):
   @staticmethod
   def Show(txn, state):
      txn.ui.ShowFinish(txn.success, txn.message)
      txn.Quit()

class PromptInstall(State):
   @staticmethod
   def Show(txn, state):
      txn.ui.ShowPromptInstall()
      txn.Next()

def ShowMessage(messageType, message, useWrapper=True):
   """
   Write a message to the terminal

   If messageType is of greater or equal to severity of
   MessageType.WARNING the message is written to stderr.  Otherwise
   the message is written to stdout.

   @param messageType: one of MessageType
   @param message: message text to display
   @param useWrapper: Bool: Wrap message text or not
   """
   if messageType >= MessageTypes.WARNING:
      output = sys.stderr
   else:
      output = sys.stdout

   if useWrapper:
      output.write('%s\n' % Format(message))
   else:
      output.write('%s\n' % message)

class Wizard(object):
   """ Console UI wizard """
   HEADER = '['
   FOOTER = ']'
   PERCENT = '%4s%%'
   WIDTH = 70

   def __init__(self, txn):
      # curses must be setup here, otherwise it will fail when run
      # from a dumb terminal.
      curses.setupterm()

      COMMANDS = \
          {'CURSOR_INVISIBLE': 'civis',
           'CURSOR_RESET': 'cnorm',
           'CLEAR': 'el',
           'RESET_ATTR': 'sgr0',
           'MOVE_DOWN': 'cud1',
           'MOVE_UP': 'cuu1',
           'BOLD': 'bold',}

      for name, cmd in COMMANDS.iteritems():
         globals()[name] = curses.tigetstr(cmd)

      self._printed = 0
      self._addNewline = False
      self._cursorEnabled = True
      self._curLine = 0         # Line 0: Primary message
                                # Line 1: Secondary message
                                # Line 2: Progress bar
      self._lowestLine = 0

      # Set up App Control.
      try:
         self.appControl = UIAppControl()
      except:
         self.appControl = None

   def ShowFinish(self, success, message):
      """ Clean up and restore console state """
      self.EnableCursor(True)
      self._moveLine(self._lowestLine)

      # Newline before printing our final message.
      print

      if success:
         print Format(message)
      else:
         print >> sys.stderr, Format(message)

   def UserMessage(self, messageType, message, useWrapper=False):
      """ In console, a passthrough to ShowMessage """
      ShowMessage(messageType, message, useWrapper=useWrapper)

   def ShowPromptInstall(self):
      raw_input(Format('The product is ready to be installed.  Press Enter to begin '
                       'installation or Ctrl-C to cancel.'))
      print

   def ShowEULA(self, text, productName):
      """ Display a EULA and ask for acceptance """
      raw_input(Format('You must accept the %s End User License'
                       ' Agreement to continue.  Press Enter to proceed.' % productName))

      # @fixme 1.0:  need some sane way to handle helpers like this
      try:
         # The EULA should be given without any wrapping already done
         # to it.  However, TextWrapper assumes that the text has not been
         # formatted already.  Since the EULA contains line breaks, this
         # throws off TextWrapper's formatting.  So we need to format it line
         # by line instead of in one large chunk.
         wrapper = TextWrapper()
         wrapper.width = 79
         wrapper.replace_whitespace = False # Needed to preserve paragraph spacing.
         EUList = text.encode('utf-8', 'ignore').split('\n')
         text = ""
         for line in EUList:
            text = ''.join((text, wrapper.fill(line), '\n'))

         Popen(self._getMoreBin(), stdin=PIPE).communicate(input=text)
      except IOError, e: # RHEL4 appears to close stdin while we still expect it to be open
         pass

      return self.Prompt('Do you agree? [yes/no]', '')

   def _getMoreBin(self):
      """
      Respect user's $PAGER (or fallback to `more' if none is specified).
      If $PAGER is set to `less', add the `-E' flag to exit as soon as
      EOF is reached.

      @return: If the pager is found, a string or list of strings containing the
      command to execute the pager.  'more' if no alternative is found.
      """
      pager = os.environ.get('PAGER')

      if pager:
         # If the pager is not an absolute path, search the
         # path for it.
         if not path(pager).isabs():
            pager = Which(pager)
         elif not path(pager).isexe():
            # Verify that the pager exists and is executable.
            # Don't just assume that it's been set correctly.
            # This keeps us from crashing.
            pager = None

      # If no pager is found, or cannot be found in
      # the path, default to 'more'.
      if not pager:
         pager = 'more'

      # Append -E to less so it quits after the last line of
      # the EULA is displayed.
      if path(pager).basename() == 'less':
         pager = (pager, '-E')

      return pager

   def Prompt(self, text, default, format=True):
      """
      Prompt for an answer

      @fixme: This is rather awkward because we have hijacked SIGINT for
      our own _abort to prevent cancellation when we're in an
      inconsistent state.  However, we need a way to cancel from here.
      Right now we're doing that by letting the EOFError pass through.
      """
      defaultTxt = default and ' [%s]' % default or ''

      if format:
         answer = raw_input('%s: ' % Format(text + defaultTxt))
      else:
         answer = raw_input('%s: ' % (text + defaultTxt))
      print

      if not answer:
         answer = default

      return answer

   def SetProgress(self, fraction):
      """ Print a progress bar at the given fraction """
      self._moveLine(PROGRESS_LINE)

      percent = min(fraction * 100, 100)

      if self._cursorEnabled:
         self.EnableCursor(False)    # Disable blinking cursor
         self._cursorEnabled = False

      self._addNewline = True

      if percent < 0 or percent > 100:
         return

      percent = int(math.ceil(percent))
      scaled = percent * self.WIDTH / 100

      self._moveBeginOfLine()

      write = self.HEADER
      self._printed += len(write)
      sys.stdout.write(write)

      write = '#' * scaled
      self._printed += len(write)
      sys.stdout.write(write)

      write = ' ' * (self.WIDTH - scaled)
      self._printed += len(write)
      sys.stdout.write(write)

      write = self.FOOTER
      self._printed += len(write)
      sys.stdout.write(write)

      write = self.PERCENT % percent
      self._printed += len(write)
      sys.stdout.write(write)

      sys.stdout.flush()

   def EnableCursor(self, enable):
      """ Enable or disable the cursor """
      if enable and not self._cursorEnabled:
         sys.stdout.write(CURSOR_RESET)
      elif not enable and self._cursorEnabled:
         sys.stdout.write(CURSOR_INVISIBLE)

      self._cursorEnabled = enable

      sys.stdout.flush()

   def _moveBeginOfLine(self):
      """ Move to the beginning of the current line """
      sys.stdout.write(curses.tparm(curses.tigetstr('hpa'), 0))
      sys.stdout.flush()

   def SetPrimaryText(self, txt):
      pass

   def SetSecondaryText(self, txt):
      pass

   def EnableBack(self, enabled):
      pass

   def EnableNext(self, enabled):
      pass

   def HideCancel(self):
      pass

   def HideBack(self):
      pass

   def HideNext(self):
      pass

   def SetTitle(self, title):
      pass

   def EnableCancel(self, enable):
      """ Enable/disable cancellation by setting or ignoring SIGINT """
      # EnableCancel(True) might be called even if cancellation is
      # enabled so we must key off the previously saved state as well.
      #
      # XXX: use hasattr because null UI doesn't use console's
      # __init__.
      # If we've never set this variable, define it now to False.  We need
      # this so self._cancelFunction doesn't get overridden by two calls
      # with enable set to False.
      if not hasattr(self, '_cancelEnabled'):
         self._cancelEnabled = False

      if enable and not self._cancelEnabled and hasattr(self, '_cancelFunction'):
         self._cancelEnabled = True
         signal.signal(signal.SIGINT, self._cancelFunction)

      if not enable and self._cancelEnabled:
         self._cancelFunction = signal.getsignal(signal.SIGINT)
         signal.signal(signal.SIGINT, signal.SIG_IGN)
         self._cancelEnabled = False

   def SetNextType(self, type):
      pass

   def _moveLine(self, line):
      """ Move the cursor to given line number at the beginning of the line """
      move = line - self._curLine

      self._moveBeginOfLine()

      if move < 0:              # up
         move = abs(move)
         cmd = MOVE_UP
      elif move > 0:            # down
         cmd = MOVE_DOWN
      else:                     # stay
         cmd = ''

      self._curLine = line
      self._lowestLine = max(self._lowestLine, line)

      sys.stdout.write(cmd * move)
      sys.stdout.flush()

   def SetPrimaryProgressMessage(self, text):
      self._moveLine(PRIMARY_LINE)

      sys.stdout.write(CLEAR)
      sys.stdout.write(BOLD)
      sys.stdout.write(text)
      sys.stdout.write(RESET_ATTR)

      sys.stdout.flush()

   def SetSecondaryProgressMessage(self, text):
      self._moveLine(SECONDARY_LINE)

      sys.stdout.write(CLEAR)
      sys.stdout.flush()
      sys.stdout.write('    %s' % text)

      sys.stdout.flush()

   def ShowProgress(self):
      """ Initialize progress display """
      sys.stdout.write('\n' * 2) # Create empty lines to display messages and progress
      sys.stdout.flush()

      self._curLine = PROGRESS_LINE

if __name__ == '__main__':
   ui = Wizard(None, None, None)

   ui.SetPrimaryProgressMessage('Primary')
   ui.SetSecondaryProgressMessage('Secondary')

   for i in range(0, 101):
      ui.SetProgress(i / 100.0)
      time.sleep(.002)

   ui.ShowFinish()
